package com.kalenchuk.ui.combo;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collections;

import javax.swing.JComboBox;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.plaf.basic.BasicComboBoxEditor;

/**
 * This class uses AutoCompleteComboBoxEditor and AutoCompleteTextField to offer
 * and Auto Completion to typed text based on the Objects added to the ComboBox.
 * ** IMPORTANT ** Since this is a text-based utility any Object added to this
 * ComboBox (even those that you create) MUST, I repeat MUST have a valid
 * toString method that returns the portion text representation of your
 * 
 * @author Brandon Buck
 * @version 1.0
 */
public class AutoCompleteComboBox<E> extends JComboBox {
	private AutoCompleteTextField editorComp;

	public AutoCompleteComboBox() {
		super();
		AutoCompleteComboBoxEditor cbe = new AutoCompleteComboBoxEditor();
		this.editorComp = (AutoCompleteTextField) (cbe.getEditorComponent());
		this.setEditor(cbe);
		this.setEditable(true);
	}

	@Override
	public void addItem(Object anObject) {
		super.addItem(anObject);
		editorComp.addPossibility(anObject.toString());
	}

	@Override
	public void removeAllItems() {
		super.removeAllItems();
		editorComp.removeAllPossibilities();
	}

	public AutoCompleteTextField getEditorComponent() {
		return editorComp;
	}

	@Override
	public void removeItemAt(int index) {
		E anObject = getItemAt(index);
		super.removeItemAt(index);
		editorComp.removePossibility(anObject.toString());
	}

	@Override
	public E getItemAt(int index) {
		return (E) super.getItemAt(index);
	}

	@Override
	public E getSelectedItem() {
		return (E) super.getSelectedItem();
	}
}

/**
 * A JTextField that will display a whole word as the user types a portion of
 * it. A better example would be, as the user types the first letter in the word
 * "text" the AutoCompleteTextField will display the letter "t" in the
 * components current foreground color and display the rest of the word "ext" in
 * the specified incomplete color (default is Color.GRAY.brighter()). The
 * displayed text changes as the user types "e" so now getText() would return
 * the string "te" and the incomplete portion displayed by the editor will be
 * "xt." If Enter is pressed while an incomplete word is being processes, then
 * the AutoCompleteTextField will replace what he user has typed with the
 * completed word and consider itself "finished," and if Enter is pressed a
 * second time, then it will fire a KeyEvent normally.
 * 
 * @author Brandon Buck
 * @version 1.0
 */
class AutoCompleteTextField extends JTextField implements KeyListener, DocumentListener {
	private ArrayList<String> possibilities;
	private int currentGuess;
	private Color incompleteColor;
	private boolean areGuessing;
	private boolean caseSensitive;

	/**
	 * Constructs a new AutoCompleteTextField with the 5 columns by default and
	 * no case sensitivity on comparisons of guesses to entered text.
	 */
	public AutoCompleteTextField() {
		this(5, false);
	}

	/**
	 * Constructs a new AutoCompleteTextField with the specified number of
	 * columns with no case sensitivity when comparing the current guess to the
	 * text entered.
	 * 
	 * @param columns
	 *            The number of columns you wish for the width of this
	 *            AutoCompleteTextField.
	 */
	public AutoCompleteTextField(int columns) {
		this(columns, false);
	}

	/**
	 * Creates a new AutoCompleteTextField with the given number of columns in
	 * width and the setting for case sensitivity when comparing the current
	 * guess to the entered text.
	 * 
	 * @param columns
	 *            The number of columns of text for this component.
	 * @param caseSensitive
	 *            <code>true</code> or <code>false</code> depending on if you
	 *            want comparisons to be case sensitive or not.
	 */
	public AutoCompleteTextField(int columns, boolean caseSensitive) {
		super.setColumns(columns);
		this.possibilities = new ArrayList<String>();
		this.incompleteColor = Color.GRAY.brighter();
		this.currentGuess = -1;
		this.areGuessing = false;
		this.caseSensitive = caseSensitive;
		this.addKeyListener(this);
		this.getDocument().addDocumentListener(this);
	}

	/**
	 * Add a new possibility to the list of possibilities. Add a new possibility
	 * to the list of possibilities for the AutoCommpleteTextField to process.
	 * 
	 * @param possibility
	 *            The new possibility to add.
	 */
	public void addPossibility(String possibility) {
		this.possibilities.add(possibility);
		Collections.sort(possibilities);
	}

	/**
	 * Removes a possibility from the list of possibilities. Removes the given
	 * possibility from the list of possibilities so that it will no longer be
	 * matched.
	 * 
	 * @param possibility
	 *            The possibility to remove.
	 */
	public void removePossibility(String possibility) {
		this.possibilities.remove(possibility);
	}

	/**
	 * Removes all possibilities in the list. Removes every possibility in the
	 * list and starts over with an empty list ready for new possibilities to be
	 * added.
	 */
	public void removeAllPossibilities() {
		this.possibilities = new ArrayList<String>();
	}

	/**
	 * Sets the color to draw the incomplete guess in. This sets the color that
	 * the incomplete guess text is drawn in.
	 * 
	 * @param incompleteColor
	 *            The new color to draw the incomplete guess with.
	 */
	public void setIncompleteColor(Color incompleteColor) {
		this.incompleteColor = incompleteColor;
	}

	/**
	 * Returns the current guess from the list of possibilities. Returns the
	 * string at the location of the current guess in the list of possibilities.
	 * 
	 * @return The current guess as a String.
	 */
	private String getCurrentGuess() {
		if (this.currentGuess != -1)
			return this.possibilities.get(this.currentGuess);

		return this.getText();
	}

	/**
	 * Changes the current case sensitive setting to the given setting.
	 * 
	 * @param caseSensitive
	 *            <code>true</code> or <code>false</code> depending on if you
	 *            want comparisons to be case sensitive or not.
	 */
	public void setCaseSensitive(boolean caseSensitive) {
		this.caseSensitive = caseSensitive;
	}

	private void findCurrentGuess() {
		String entered = this.getText();
		if (!this.caseSensitive)
			entered = entered.toLowerCase();

		for (int i = 0; i < this.possibilities.size(); i++) {
			currentGuess = -1;

			String possibility = this.possibilities.get(i);
			if (!this.caseSensitive)
				possibility = possibility.toLowerCase();
			if (possibility.startsWith(entered)) {
				this.currentGuess = i;
				break;
			}
		}
	}

	@Override
	public void setText(String text) {
		super.setText(text);
		this.areGuessing = false;
		this.currentGuess = -1;
	}

	@Override
	public void paintComponent(Graphics g) {
		String guess = this.getCurrentGuess();
		String drawGuess = guess;

		super.paintComponent(g);
		String entered = this.getText();
		Rectangle2D enteredBounds = g.getFontMetrics().getStringBounds(entered, g);

		if (!(this.caseSensitive)) {
			entered = entered.toLowerCase();
			guess = guess.toLowerCase();
		}

		if (!(guess.startsWith(entered)))
			this.areGuessing = false;

		if (entered != null && !(entered.equals("")) && this.areGuessing) {
			String subGuess = drawGuess.substring(entered.length(), drawGuess.length());
			Rectangle2D subGuessBounds = g.getFontMetrics().getStringBounds(drawGuess, g);

			int centeredY = ((getHeight() / 2) + (int) (subGuessBounds.getHeight() / 2));

			g.setColor(this.incompleteColor);
			g.drawString(subGuess + "   press ENTER to send or \u2192 to fill", (int) (enteredBounds.getWidth()) + 2, centeredY - 2);
		}
	}

	public void keyTyped(KeyEvent e) {
	}

	public void keyPressed(KeyEvent e) {
		if (e.getKeyCode() == KeyEvent.VK_ENTER) {
			if (this.areGuessing) {
				this.setText(this.getCurrentGuess());
				this.areGuessing = false;
			}
		}

		if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
			if (this.areGuessing) {
				this.setText(this.getCurrentGuess());
				this.areGuessing = false;
				e.consume();
			}
		}
	}

	public void keyReleased(KeyEvent e) {
	}

	public void insertUpdate(DocumentEvent e) {
		String temp = this.getText();

		if (temp.length() == 1)
			this.areGuessing = true;

		if (this.areGuessing)
			this.findCurrentGuess();

	}

	public void removeUpdate(DocumentEvent e) {
		String temp = this.getText();

		if (!(this.areGuessing))
			this.areGuessing = true;

		if (temp.length() == 0)
			this.areGuessing = false;
		else if (this.areGuessing) {
			this.findCurrentGuess();
		}
	}

	public void changedUpdate(DocumentEvent e) {
	}
}

class AutoCompleteComboBoxEditor extends BasicComboBoxEditor {
	public AutoCompleteComboBoxEditor() {
		editor = createEditorComponent();
	}

	@Override
	protected AutoCompleteTextField createEditorComponent() {
		AutoCompleteTextField acEditor = new AutoCompleteTextField(9);
		acEditor.setBorder(null);
		return acEditor;
	}
}
